/*
 * Copyright:  2018 smarabbit studio.
 *
 *  Licensed under the Confluent Community License; you may not use this file
 *  except in compliance with the License.  You may obtain a copy of the License at
 *
 *  http://www.confluent.io/confluent-community-license
 *
 *  Unless required by applicable law or agreed to in writing, software
 *  distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
 *  WARRANTIES OF ANY KIND, either express or implied.  See the License for the
 *  specific language governing permissions and limitations under the License.
 *
 *  @作   者： 黄开晖 (117227773@qq.com)
 *  @日   期:  2021年8月13日
 */
package com.massyframework.beanskin.maven.plugin;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.StandardCopyOption;
import java.security.KeyStore;
import java.security.PrivateKey;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.Enumeration;
import java.util.List;
import java.util.jar.JarEntry;
import java.util.jar.JarFile;

import org.apache.commons.codec.digest.DigestUtils;
import org.apache.maven.artifact.Artifact;
import org.apache.maven.artifact.DefaultArtifact;
import org.apache.maven.artifact.handler.ArtifactHandler;
import org.apache.maven.artifact.handler.DefaultArtifactHandler;
import org.apache.maven.execution.MavenSession;
import org.apache.maven.model.Dependency;
import org.apache.maven.model.Plugin;
import org.apache.maven.plugin.MojoExecutionException;
import org.apache.maven.plugin.MojoFailureException;
import org.apache.maven.plugins.annotations.Component;
import org.apache.maven.plugins.annotations.LifecyclePhase;
import org.apache.maven.plugins.annotations.Mojo;
import org.apache.maven.plugins.annotations.Parameter;
import org.apache.maven.project.DefaultProjectBuildingRequest;
import org.apache.maven.project.MavenProject;
import org.apache.maven.project.ProjectBuilder;
import org.apache.maven.project.ProjectBuildingException;
import org.apache.maven.project.ProjectBuildingRequest;

import com.massyframework.beanskin.maven.plugin.ras.RSAKeyReader;
import com.massyframework.beanskin.maven.plugin.ras.RSAService;
import com.massyframework.beanskin.maven.plugin.util.BouncycastleUtils;
import com.massyframework.beanskin.maven.plugin.util.IOUtils;

/**
 * 整合Mojo
 * @author huangkh
 *
 */
@Mojo(name ="integration", defaultPhase = LifecyclePhase.COMPILE)
public class IntegrationMojo extends ArtifactsMojo {
	
	private static final String METAINF               = "META-INF/";
	
	/**
     * POM
     */
    @Parameter( defaultValue = "${project}", readonly = true, required = true )
    private MavenProject project;
    
    /**
     * The Maven session
     */
    @Parameter( defaultValue = "${session}", readonly = true, required = true )
    protected MavenSession session;
    
    @Component
    private ProjectBuilder projectBuilder;
    
	@Parameter
    private Module module;
    
    /**
     * Location of the file.
     */
    @Parameter( defaultValue = "${project.build.directory}", property = "outputDir", required = true )
    private File outputDirectory;
    
    private ArtifactHandler artifactHandler = new DefaultArtifactHandler();
    
	/**
	 * 
	 */
	public IntegrationMojo() {
		
	}

	@Override
	public void execute() throws MojoExecutionException, MojoFailureException {
		String key = "com.massyframework.beanskin:beanskin-maven-plugin";
		
		Plugin plugin = this.project.getPlugin(key);
		List<Artifact> artifacts = this.loadArtifactItems(plugin, "integration");
		List<Dependency> newDependencies = this.foundUnreferenceDependencies(artifacts);
		List<File> files = this.locateJars(artifacts);
		
		File outputDir = this.clear();
		this.copyFiles(files, outputDir);
						
		newDependencies.forEach( dep -> this.project.getDependencies().add(dep));
		
		//改写pom.xml
		if (!newDependencies.isEmpty()) {
			this.addDependencyToPom(newDependencies);
		}
		
		if (this.module != null) {
			File dir = this.hasRuntimeDir(outputDir);
			if (dir != null) {
				File config = this.getConfig();
				if (config.exists()) {
					try {
						KeyStoreInformation info =
							KeyStoreInformation.newBuilder(config).build();
						
						String signature = this.sign(new File(dir, "lib"), info);
						writeSignFile(signature, dir);
					}catch(Exception e) {
						throw new MojoExecutionException(e.getMessage(), e);
					}
				}else {
					this.getLog().warn("cannot found config file:" + config.getPath() +", skip.");
				}
			}else {
				this.getLog().warn("cannot found module directory:" + this.module.getGroupId() +
						":" + this.module.getModuleId() + ":" + this.module.getVersion() +
						", skip.");
			}
		}
	}
	
	/**
	 * 添加依赖到pom文件
	 * @param dependencies
	 * @throws MojoExecutionException 
	 */
	protected void addDependencyToPom(List<Dependency> dependencies) throws MojoExecutionException {
		File pom = new File(this.project.getBasedir(), "pom.xml");
		if (pom.exists()) {
			this.getLog().info("add dependencies to Pom.xml");
			PomXmlWriter writer = new PomXmlWriter(pom);
			try {
				writer.appendDependency(dependencies);
			}catch(Exception e) {
				e.printStackTrace();
				throw new MojoExecutionException(e.getMessage(), e);
			}
		}
	}
	
	/**
	 * 清理本地输出目录文件
	 */
	protected File clear() {
		File classes = new File(this.outputDirectory, "classes");
		
		/*
		File module = new File(classes, "MODULE");
		
		if (module.exists()) {
			this.deleteDirectory(module);
		}
		*/
		
		return classes;
	}
	
	/**
	 * 清除本地文件并复制来自于Artifact内部的文件
	 * @param files {@link List},文件
	 * @param outputDir {@link File},输出目录
	 * @throws MojoExecutionException 
	 */
	protected void copyFiles(List<File> files, File outputDir) throws MojoExecutionException {
		Path path = outputDir.toPath();
		for (File file: files) {
			try {
				this.copyFile(file,  path);
			}catch(IOException e) {
				throw new MojoExecutionException(e.getMessage(), e);
			}
		}
	}
	
	protected void copyFile(File file, Path outputDir) throws IOException {
		JarFile jarFile = new JarFile(file);
		
		try {
			Enumeration<JarEntry> em = jarFile.entries();
			while (em.hasMoreElements()) {
				JarEntry entry = em.nextElement();
				if (entry.isDirectory()) {
					continue;
				}
				
				String name = entry.getName();
				if (name.startsWith(METAINF)) {
					continue;
				}
				
				InputStream is = jarFile.getInputStream(entry);
				Path outputFile = outputDir.resolve(name);

				Path parentDir = outputFile.getParent();
				if (!Files.exists(parentDir)) {
					Files.createDirectories(parentDir);
				}
				
				Files.copy(is, outputFile, StandardCopyOption.REPLACE_EXISTING);
			}
			
			this.getLog().info("copying " + file + ".");
		}catch(IOException e) {
			throw e;
		}finally {
			if (jarFile != null) {
				jarFile.close();
			}
		}
	}
	
	protected List<Dependency> foundUnreferenceDependencies(List<Artifact> artifacts) throws MojoExecutionException{
		List<Dependency> result = new ArrayList<>();
		DependencyChecker checker = new DependencyChecker(this.project.getDependencies());
		artifacts.forEach( artifact-> {
			Dependency dependency = new Dependency();
			dependency.setGroupId(artifact.getGroupId());
			dependency.setArtifactId(artifact.getArtifactId());
			dependency.setVersion(artifact.getVersion());
			checker.isNotExist(dependency);
		});
		
		ProjectBuildingRequest buildingRequest = 
				new DefaultProjectBuildingRequest(session.getProjectBuildingRequest());
		for (Artifact artifact: artifacts) {
	        try {
	        	buildingRequest.setProject(null);	        	
	            MavenProject mavenProject = projectBuilder.build(artifact, buildingRequest).getProject();
	            for (Dependency dependency: mavenProject.getDependencies()) {
	            	if (dependency.getScope().equals("provided")) {
	            		continue;
	            	}
	            	if (checker.isNotExist(dependency)) {
	            		this.getLog().info("found not reference:" + dependency + ".");
	            		result.add(dependency);
	            	}else {
	            	}
	            }
	        } catch (ProjectBuildingException e) {
	            throw new MojoExecutionException("Error while building project", e);
	        }
		}
		
		
		return result;
	}
	
	
		
	
	
	/**
	 * 是否输出运行内核
	 * @param outputDir {@link File}
	 * @return {@link File}
	 */
	protected File hasRuntimeDir(File outputDir) {
		String subPath = 
				"MODULE".concat(File.separator)
				.concat(module.getGroupId()).concat(File.separator)
				.concat(module.getModuleId()).concat(File.separator)
				.concat(module.getVersion());
		File dir = new File(outputDir, subPath);
		if (dir.exists()) {
			return dir;
		}
		
		return null;
	}
	
	protected File getConfig() {
		String dir = System.getProperty("user.home");
		String subPath = ".beanskin".concat(File.separator)
				.concat(module.getModuleId())
				.concat(".properties");
		return new File(dir, subPath);
	}
	
	
	/**
	 * 对md5进行签名
	 * @param dir
	 * @param info
	 * @return
	 * @throws Exception
	 */
	protected String sign(File dir, KeyStoreInformation info) throws Exception {
		List<File> jarFiles = this.listAndSortJarFiles(dir);
		String content = this.calculateFilesMd5(jarFiles);
	
		RSAKeyReader reader = BouncycastleUtils.createRSAKeyReader();
		File file = new File(info.getKeyStore());
		KeyStore keyStore =
				reader.loadKeyStore(file, info.getKeyStorePass().toCharArray());
		PrivateKey key =
				reader.getPrivateKey(keyStore, info.getAlias(), info.getKeyPass().toCharArray());
		String privateKey = reader.getString(key);
		
		RSAService service = BouncycastleUtils.createRasService();
		return service.sign(content, privateKey);
	}
	
	/**
	 * 写入文件
	 * @param signature
	 * @param dir
	 * @throws IOException
	 */
	protected void writeSignFile(String signature, File dir) throws IOException{
		File file = new File(dir, "extends".concat(File.separator).concat("jarsign.data"));
		if (file.exists()) {
			file.delete();
		}else {
			File parent = file.getParentFile();
			parent.mkdirs();
		}
		
		//写入数据
		OutputStream os = null;
		try {
			os = new FileOutputStream(file);
			os.write(signature.getBytes("utf-8"));
			os.flush();
			
			this.getLog().info("write jarsign.data success.");
		}finally {
			IOUtils.close(os);
		}
	}
	
	/**
	 * 计算所有文件的md5值
	 * <br>
	 * 将所有文件md5值按次序进行拼接，然后在计算拼接值的md5.
	 * @param files
	 * @return
	 * @throws IOException
	 */
	protected String calculateFilesMd5(List<File> files) throws IOException {
		StringBuilder builder = new StringBuilder();
		int size = files.size();
		for (int i=0; i< size; i++) {
			File file = files.get(i);
			String digest = this.calculateFileMd(file);
			
			builder.append(digest);
			if (i != size -1) {
				builder.append(",");
			}
		}
		
		return DigestUtils.md5Hex(builder.toString());
	}
	
	/**
	 * 计算文件摘要
	 * @return
	 */
	protected String calculateFileMd(File file) throws IOException {
		InputStream is = null;
		try {
			is = new FileInputStream(file);
			return DigestUtils.md5Hex(is);
		}finally {
			IOUtils.close(is);
		}
	}
	
	/**
	 * 列示所有jar文件，并排序
	 * @param dir {@link File},目录
	 * @return {@link List},jar文件集合
	 */
	protected List<File> listAndSortJarFiles(File dir){
		List<File> result = new ArrayList<>();
		this.doListJarFiles(dir, result);
		
		Comparator<File> comparator = (f1, f2) ->{
			String path1 = f1.getPath();
			String path2 = f2.getPath();
			return path1.compareTo(path2);
		};
		
		Collections.sort(result, comparator);
		return result;
	}
	
	protected void doListJarFiles(File dir, List<File> list) {
		if (dir.exists()) {
			File[] files = dir.listFiles();
			for (File file: files) {
				if (file.isDirectory()) {
					this.doListJarFiles(file, list);
				}else {
					if (file.getName().endsWith(".jar")) {
						list.add(file);
					}
				}
			}
		}
	}
	
	
	
	@Override
	protected ArtifactBuilder createArtifactBuilder() {
		return new Builder();
	}

	private class Builder implements ArtifactBuilder {
		
		private String groupId;
		private String artifactId;
		private String version;

		@Override
		public Builder groupId(String value) {
			this.groupId = value;
			return this;
		}

		@Override
		public Builder artifactId(String value) {
			this.artifactId = value;
			return this;
		}

		@Override
		public Builder version(String value) {
			this.version = value;
			return this;
		}

		@Override
		public Artifact build() {
			return new DefaultArtifact(groupId, artifactId, version, "compile", "jar", null,  artifactHandler);
		}
	}
}
